{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit WeatherLookup;

interface

uses Classes, XMLHelper, Mediate_TLB;

const
  FILE_WEATHER = 'WeatherCache.xml';
  FORECAST_DAYS = 3;

type
  TAHMWeatherManager = class;

  TAHMWeatherLink = class(TCollectionItem)
  private
    FURL: String;
    FDescription: String;
  published
    property URL: String read FURL write FURL;
    property Description: String read FDescription write FDescription;
  end;

  TAHMWeatherLocation = class(TCollectionItem)
  private
    FLocationID: String;
    FLocationName: String;
    FLattitude: String;
    FLongitude: String;
    FTimeZone: String;
    FCurrentTime: String;
    FSunRise: String;
    FSunSet: String;
    FObservatory: String;
  public
    procedure Clear;
  published
    property LocationID: String read FLocationID write FLocationID;
    property LocationName: String read FLocationName write FLocationName;
    property Lattitude: String read FLattitude write FLattitude;
    property Longitude: String read FLongitude write FLongitude;
    property TimeZone: String read FTimeZone write FTimeZone;
    property CurrentTime: String read FCurrentTime write FCurrentTime;
    property SunRise: String read FSunRise write FSunRise;
    property SunSet: String read FSunSet write FSunSet;
    property Observatory: String read FObservatory write FObservatory;
  end;

  TAHMWeatherUnitPreference = (wupMetric, wupImperial);
  TAHMWeatherDataType = (wlLocation, wlCurrent, wlForecast, wlAffiliate);
  TAHMWeatherTempUnit = (wtDegreesF, wtDegreesC);
  TAHMWeatherDistanceUnit = (wdMiles, wdKilometres);
  TAHMWeatherSpeedUnit = (wsMPH, wsKMH);
  TAHMWeatherRainfallUnit = (wrInch, wrMM);
  TAHMWeatherPressureUnit = (wpInches, wpMillibars);

  TAHMWeatherUnits = class(TPersistent)
  private
    FPreference: TAHMWeatherUnitPreference;
    FTempUnit: TAHMWeatherTempUnit;
    FDistUnit: TAHMWeatherDistanceUnit;
    FSpeedUnit: TAHMWeatherSpeedUnit;
    FRainUnit: TAHMWeatherRainfallUnit;
    FPresUnit: TAHMWeatherPressureUnit;
  protected
    procedure Clear;
  published
    property PreferredUnits: TAHMWeatherUnitPreference read FPreference write FPreference;
    property TempUnit: TAHMWeatherTempUnit read FTempUnit write FTempUnit;
    property DistanceUnit: TAHMWeatherDistanceUnit read FDistUnit write FDistUnit;
    property SpeedUnit: TAHMWeatherSpeedUnit read FSpeedUnit write FSpeedUnit;
    property RainfallUnit: TAHMWeatherRainfallUnit read FRainUnit write FRainUnit;
    property PressureUnit: TAHMWeatherPressureUnit read FPresUnit write FPresUnit;
  end;

  TAHMWeatherData = class(TPersistent)
  private
    FLastChecked: TDateTime;
    FLastUpdated: TDateTime;
  protected
    function GetLastUpdatedAsString: String;
  public
    procedure Clear; virtual;
    property LastUpdatedAsString: String read GetLastUpdatedAsString;
  published
    property LastChecked: TDateTime read FLastChecked write FLastChecked;
    property LastUpdated: TDateTime read FLastUpdated write FLastUpdated;
  end;

  TAHMWeatherStandardData = class(TPersistent)
  private
    FImage: String;
    FDescription: String;
    FWindSpeed: Single;
    FWindGusts: Single;
    FWindDirection: String;
    FHumidity: Single;
    FRainLikelihood: Single;
  protected
    procedure Clear;
    function GetWindSpeedAsString: String;
    function GetWindGustsAsString: String;
    function GetHumidityAsString: String;
    function GetRainLikelihoodAsString: String;
  public
    property WindSpeedAsString: String read GetWindSpeedAsString;
    property WindGustsAsString: String read GetWindGustsAsString;
    property HumidityAsString: String read GetHumidityAsString;
    property RainLikelihoodAsString: String read GetRainLikelihoodAsString;
  published
    property Image: String read FImage write FImage;
    property Description: String read FDescription write FDescription;
    property WindSpeed: Single read FWindSpeed write FWindSpeed;
    property WindGusts: Single read FWindGusts write FWindGusts;
    property WindDirection: String read FWindDirection write FWindDirection;
    property Humidity: Single read FHumidity write FHumidity;
    property RainLikelihood: Single read FRainLikelihood write FRainLikelihood;
  end;

  TAHMWeatherLocationData = class(TAHMWeatherData)
  private
    FLocations: TCollection;
    FCurrentLocation: TAHMWeatherLocation;
  protected
    property Locations: TCollection read FLocations;
  public
    constructor Create;
    destructor Destroy; override;
  published
    property CurrentLocation: TAHMWeatherLocation read FCurrentLocation;
  end;

  TAHMWeatherAffiliateData = class(TAHMWeatherData)
  private
    FWeatherLinks: TCollection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear; override;
  published
    property WeatherLinks: TCollection read FWeatherLinks;
  end;

  TAHMWeatherCurrentData = class(TAHMWeatherData)
  private
    FWeather: TAHMWeatherStandardData;
    FActualTemp: Single;
    FPerceivedTemp: Single;
    FPressure: Single;
    FPressureChange: String;
    FVisibility: Single;
    FUVIndex: Integer;
    FUVDescription: String;
    FDewpoint: Single;
    FMoonImage: String;
    FMoonDescription: String;
  protected
    function GetActualTemperatureAsString: String;
    function GetPerceivedTemperatureAsString: String;
    function GetPressureAsString: String;
    function GetVisibilityAsString: String;
    function GetUVIndexAsString: String;
    function GetDewpointAsString: String;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear; override;
    property ActualTemperatureAsString: String read GetActualTemperatureAsString;
    property PerceivedTemperatureAsString: String read GetPerceivedTemperatureAsString;
    property PressureAsString: String read GetPressureAsString;
    property VisibilityAsString: String read GetVisibilityAsString;
    property UVIndexAsString: String read GetUVIndexAsString;
    property DewpointAsString: String read GetDewpointAsString;
  published
    property Weather: TAHMWeatherStandardData read FWeather;
    property ActualTemperature: Single read FActualTemp write FActualTemp;
    property PerceivedTemperature: Single read FPerceivedTemp write FPerceivedTemp;
    property Pressure: Single read FPressure write FPressure;
    property PressureChange: String read FPressureChange write FPressureChange;
    property Visibility: Single read FVisibility write FVisibility;
    property UVIndex: Integer read FUVIndex write FUVIndex;
    property UVDescription: String read FUVDescription write FUVDescription;
    property Dewpoint: Single read FDewpoint write FDewpoint;
    property MoonImage: String read FMoonImage write FMoonImage;
    property MoonDescription: String read FMoonDescription write FMoonDescription;
  end;

  TAHMWeatherForecastData = class(TCollectionItem)
  private
    FDay: String;
    FDate: String;
    FLowTemp: Single;
    FHighTemp: Single;
    FSunRise: String;
    FSunSet: String;
    FForecastAM: TAHMWeatherStandardData;
    FForecastPM: TAHMWeatherStandardData;
  protected
    function GetLowTemperatureAsString: String;
    function GetHighTemperatureAsString: String;
  public
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
    property LowTemperatureAsString: String read GetLowTemperatureAsString;
    property HighTemperatureAsString: String read GetHighTemperatureAsString;
  published
    property ForecastAM: TAHMWeatherStandardData read FForecastAM;
    property ForecastPM: TAHMWeatherStandardData read FForecastPM;
    property Day: String read FDay write FDay;
    property Date: String read FDate write FDate;
    property LowTemperature: Single read FLowTemp write FLowTemp;
    property HighTemperature: Single read FHighTemp write FHighTemp;
    property SunRise: String read FSunRise write FSunRise;
    property SunSet: String read FSunSet write FSunSet;
  end;

  TAHMWeatherForecasts = class(TAHMWeatherData)
  private
    FForecasts: TCollection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear; override;
  published
    property Forecasts: TCollection read FForecasts;
  end;

  TAHMWeatherLookupService = class
  private
    FUpdateWeather: Boolean;
    FUpdateForecast: Boolean;
    FUpdateAffiliate: Boolean;
  protected
    FXML: TAHMXMLHelper;
    FWeather: TAHMWeatherManager;
    FForecastDays: Integer;
    property ShouldUpdateWeather: Boolean read FUpdateWeather;
    property ShouldUpdateForecast: Boolean read FUpdateForecast;
    property ShouldUpdateAffiliate: Boolean read FUpdateAffiliate;
    function RetrieveXML(URL: String): Boolean;
    function CheckTermsOfService(Request: TAHMWeatherDataType): Boolean; virtual;
    function ParseLocation(Location: String): Boolean; virtual;
    function ParseWeather: Boolean; virtual;
    procedure UpdateLocationMeta;
    procedure UpdateWeatherMeta;
    procedure UpdateForecastMeta;
    procedure UpdateAffiliateMeta;
  public
    constructor Create(Owner: TAHMWeatherManager);
    destructor Destroy; override;
    property ForecastDays: Integer read FForecastDays write FForecastDays;
    function CheckLocation(Location: String): Boolean;
    function CheckWeather: Boolean;
  end;

  TAHMWeatherChannelLookupService = class(TAHMWeatherLookupService)
  private
    FPartnerID: String;
    FLicenseKey: String;
    function GetPreferredUnits: String;
    function ParseLastUpdated(LastUpdated: String): TDateTime;
    function ParseImage(WeatherIndex: String): String;
    procedure ParseDetailHeader(RootNode: TAHMXMLNode);
    procedure ParseDetailLocation(RootNode: TAHMXMLNode);
    procedure ParseDetailConditions(RootNode: TAHMXMLNode);
    procedure ParseDetailBarometer(RootNode: TAHMXMLNode);
    procedure ParseDetailWind(RootNode: TAHMXMLNode; WeatherData: TAHMWeatherStandardData);
    procedure ParseDetailUV(RootNode: TAHMXMLNode);
    procedure ParseDetailMoon(RootNode: TAHMXMLNode);
    procedure ParseDetailForecasts(RootNode: TAHMXMLNode);
    procedure ParseDetailForecast(RootNode: TAHMXMLNode);
    procedure ParseDetailForecastPart(RootNode: TAHMXMLNode);
    procedure ParseDetailAffiliates(RootNode: TAHMXMLNode);
  protected
    function CheckTermsOfService(Request: TAHMWeatherDataType): Boolean; override;
    function ParseLocation(Location: String): Boolean; override;
    function ParseWeather: Boolean; override;
  public
    constructor Create(Owner: TAHMWeatherManager);
  end;

  TAHMWeatherManager = class(TPersistent)
  private
    FFilename: String;
    FMediate: IMediateServer;
    FWeatherUnits: TAHMWeatherUnits;
    FWeatherLookup: TAHMWeatherLookupService;
    FWeatherLocation: TAHMWeatherLocationData;
    FWeatherAffiliate: TAHMWeatherAffiliateData;
    FWeatherCurrent: TAHMWeatherCurrentData;
    FWeatherForecasts: TAHMWeatherForecasts;
  protected
    procedure LogMessage(Level: LoggingLevel; LogMessage: String);
    function GetHTTPString(const URL: String): String;
    function IsConnected: Boolean;
    function EncodeParams(const Params: String): String;
    procedure ClearMetaData(Key: String);
    procedure SetMetaData(Key, Value: String);
    function SearchLocation(Location: String): Boolean;
    function ValidateConfiguration: Boolean;
  public
    constructor Create(Server: IMediateServer);
    destructor Destroy; override;
    procedure UpdateWeatherData;
    procedure SearchWeatherLocations;
    procedure UpdateConfiguration;
  published
    property Units: TAHMWeatherUnits read FWeatherUnits;
    property LocationData: TAHMWeatherLocationData read FWeatherLocation;
    property AffiliateData: TAHMWeatherAffiliateData read FWeatherAffiliate;
    property CurrentData: TAHMWeatherCurrentData read FWeatherCurrent;
    property ForecastData: TAHMWeatherForecasts read FWeatherForecasts;
  end;


implementation

uses SysUtils, DateUtils, PersistenceHelper, WeatherConsts, MetaConsts;

var
  WeatherUnitsInstance: TAHMWeatherUnits;


// TAHMWeatherLocation

procedure TAHMWeatherLocation.Clear;
begin
  FLocationID := '';
  FLocationName := '';
  FLattitude := '';
  FLongitude := '';
  FTimeZone := '';
  FSunRise := '';
  FSunSet := '';
  FCurrentTime := '';
end;

// TAHMWeatherData

function TAHMWeatherData.GetLastUpdatedAsString: String;
var
  sFormat: String;
begin
  // First check whether last update was today
  if CompareDate(Today, FLastUpdated) = 0 then
    sFormat := 'h:nn am/pm'
  else
  begin
    // Calculate date st/nd/rd/th elements
    case DayOf(FLastUpdated) of
      1,21,31: sFormat := '"st"';
      2,22: sFormat := '"nd"';
      3,23: sFormat := '"rd"';
      else sFormat := '"th"';
    end;
    sFormat := 'dddd d' + sFormat + ' mmmm';
  end;

  // Return result in either "Monday 1st August" or "10:20 pm" format
  Result := FormatDateTime(sFormat, FLastUpdated);
end;

procedure TAHMWeatherData.Clear;
begin
  // Descendants may override this to reset additional items
  FLastChecked := 0;
  FLastUpdated := 0;
end;

// TAHMWeatherUnits

procedure TAHMWeatherUnits.Clear;
begin
  // Reset weather units according to our preference
  if FPreference = wupMetric then
  begin
    FTempUnit := wtDegreesC;
    FDistUnit := wdKilometres;
    FSpeedUnit := wsKMH;
    FRainUnit := wrMM;
    FPresUnit := wpMillibars;
  end
  else
  begin
    FTempUnit := wtDegreesF;
    FDistUnit := wdMiles;
    FSpeedUnit := wsMPH;
    FRainUnit := wrInch;
    FPresUnit := wpInches;
  end;
end;

// TAHMWeatherStandardData

procedure TAHMWeatherStandardData.Clear;
begin
  FImage := '';
  FDescription := '';
  FWindSpeed := 0.0;
  FWindGusts := 0.0;
  FWindDirection := '';
  FHumidity := 0.0;
  FRainLikelihood := 0.0;
end;

function TAHMWeatherStandardData.GetWindSpeedAsString: String;
begin
  case WeatherUnitsInstance.SpeedUnit of
    wsMPH: Result := Format('%.2f mph', [FWindSpeed]);
    wsKMH: Result := Format('%.2f km/h', [FWindSpeed]);
    else   Result := '';
  end;
end;

function TAHMWeatherStandardData.GetWindGustsAsString: String;
begin
  case WeatherUnitsInstance.SpeedUnit of
    wsMPH: Result := Format('%.2f mph', [FWindGusts]);
    wsKMH: Result := Format('%.2f km/h', [FWindGusts]);
    else   Result := '';
  end;
end;

function TAHMWeatherStandardData.GetHumidityAsString: String;
begin
  Result := Format('%.0f %%', [FHumidity]);
end;

function TAHMWeatherStandardData.GetRainLikelihoodAsString: String;
begin
  Result := Format('%.0f %%', [FRainLikelihood]);
end;

// TAHMWeatherLocationData

constructor TAHMWeatherLocationData.Create;
begin
  inherited;

  FLocations := TCollection.Create(TAHMWeatherLocation);
  FCurrentLocation := TAHMWeatherLocation.Create(nil);

  FCurrentLocation.Clear;
end;

destructor TAHMWeatherLocationData.Destroy;
begin
  FLocations.Free;

  inherited;
end;

// TAHMWeatherAffiliateData

constructor TAHMWeatherAffiliateData.Create;
begin
  inherited;

  FWeatherLinks := TCollection.Create(TAHMWeatherLink);
end;

destructor TAHMWeatherAffiliateData.Destroy;
begin
  FWeatherLinks.Free;

  inherited;
end;

procedure TAHMWeatherAffiliateData.Clear;
begin
  inherited;

  FWeatherLinks.Clear;
end;

// TAHMWeatherCurrentData

constructor TAHMWeatherCurrentData.Create;
begin
  inherited Create;

  FWeather := TAHMWeatherStandardData.Create;
end;

destructor TAHMWeatherCurrentData.Destroy;
begin
  FWeather.Free;

  inherited;
end;

procedure TAHMWeatherCurrentData.Clear;
begin
  inherited;

  FActualTemp := 0.0;
  FPerceivedTemp := 0.0;
  FPressure := 0.0;
  FPressureChange := '';
  FVisibility := 0.0;
  FUVIndex := 0;
  FUVDescription := '';
  FDewpoint := 0.0;
  FMoonImage := '';
  FMoonDescription := '';

  // Clear standard weather properties
  FWeather.Clear;
end;

function TAHMWeatherCurrentData.GetActualTemperatureAsString: String;
begin
  case WeatherUnitsInstance.TempUnit of
    wtDegreesF: Result := Format('%.0fF', [FActualTemp]);
    wtDegreesC: Result := Format('%.0fC', [FActualTemp]);
    else        Result := '';
  end;
end;

function TAHMWeatherCurrentData.GetPerceivedTemperatureAsString: String;
begin
  case WeatherUnitsInstance.TempUnit of
    wtDegreesF: Result := Format('%.0fF', [FPerceivedTemp]);
    wtDegreesC: Result := Format('%.0fC', [FPerceivedTemp]);
    else        Result := '';
  end;
end;

function TAHMWeatherCurrentData.GetPressureAsString: String;
begin
  case WeatherUnitsInstance.PressureUnit of
    wpInches:    Result := Format('%.1f in', [FPressure]);
    wpMillibars: Result := Format('%.1f mb', [FPressure]);
    else         Result := '';
  end;
end;

function TAHMWeatherCurrentData.GetVisibilityAsString: String;
begin
  case WeatherUnitsInstance.DistanceUnit of
    wdMiles:      Result := Format('%.2f miles', [FVisibility]);
    wdKilometres: Result := Format('%.2f km',    [FVisibility]);
    else          Result := '';
  end;
end;

function TAHMWeatherCurrentData.GetUVIndexAsString: String;
begin
  Result := IntToStr(FUVIndex);
end;

function TAHMWeatherCurrentData.GetDewpointAsString: String;
begin
  case WeatherUnitsInstance.TempUnit of
    wtDegreesF: Result := Format('%.0fF', [FDewpoint]);
    wtDegreesC: Result := Format('%.0fC', [FDewpoint]);
    else        Result := '';
  end;
end;

// TAHMWeatherForecastData

constructor TAHMWeatherForecastData.Create(Collection: TCollection);
begin
  inherited;

  FForecastAM := TAHMWeatherStandardData.Create;
  FForecastPM := TAHMWeatherStandardData.Create;
end;

destructor TAHMWeatherForecastData.Destroy;
begin
  FForecastAM.Free;
  FForecastPM.Free;

  inherited;
end;

function TAHMWeatherForecastData.GetLowTemperatureAsString: String;
begin
  case WeatherUnitsInstance.TempUnit of
    wtDegreesF: Result := Format('%.0fF', [FLowTemp]);
    wtDegreesC: Result := Format('%.0fC', [FLowTemp]);
    else        Result := '';
  end;
end;

function TAHMWeatherForecastData.GetHighTemperatureAsString: String;
begin
  case WeatherUnitsInstance.TempUnit of
    wtDegreesF: Result := Format('%.0fF', [FHighTemp]);
    wtDegreesC: Result := Format('%.0fC', [FHighTemp]);
    else        Result := '';
  end;
end;

// TAHMWeatherForecasts

constructor TAHMWeatherForecasts.Create;
begin
  inherited;

  FForecasts := TCollection.Create(TAHMWeatherForecastData);
end;

destructor TAHMWeatherForecasts.Destroy;
begin
  FForecasts.Free;

  inherited;
end;

procedure TAHMWeatherForecasts.Clear;
begin
  inherited;

  FForecasts.Clear;
end;

// TAHMWeatherLookupService

constructor TAHMWeatherLookupService.Create(Owner: TAHMWeatherManager);
begin
  inherited Create;

  FWeather := Owner;
  FXML := TAHMXMLHelper.Create;
  FForecastDays := FORECAST_DAYS;
end;

destructor TAHMWeatherLookupService.Destroy;
begin
//  FXML.Free;

  inherited;
end;

function TAHMWeatherLookupService.RetrieveXML(URL: String): Boolean;
var
  Response: String;
begin
  // Set default result - failed
  Result := False;

  if FWeather.IsConnected then
  begin
    FWeather.LogMessage(llDebug, 'Request URL: ' + URL);
    Response := FWeather.GetHTTPString(URL);
    FWeather.LogMessage(llDebug, 'Response XML: ' + Response);
    try
      if Response <> '' then
      begin
        FXML.DocumentXML := Response;
        Result := True;
      end;
    except
      FWeather.LogMessage(llError, 'Failed to retrieve weather XML from URL: ' + URL);
    end;
  end;
end;

function TAHMWeatherLookupService.CheckTermsOfService(Request: TAHMWeatherDataType): Boolean;
begin
  // Descendants will override this to determine which services are available
  Result := False;
end;

function TAHMWeatherLookupService.ParseLocation(Location: String): Boolean;
begin
  // Clear current locations
  FWeather.LocationData.Locations.Clear;
  FWeather.LocationData.LastChecked := Now;

  // Descendants will override this to implement location lookup
  Result := False;
end;

function TAHMWeatherLookupService.ParseWeather: Boolean;
begin
  // Clear current weather conditions if about to update
  if FUpdateWeather then
  begin
    FWeather.CurrentData.Clear;
    FWeather.CurrentData.LastChecked := Now;
  end;

  // Clear current weather forecasts if about to update
  if FUpdateForecast then
  begin
    FWeather.ForecastData.Clear;
    FWeather.ForecastData.LastChecked := Now;
  end;

  // Clear current affiliate links if about to update
  if FUpdateAffiliate then
  begin
    FWeather.AffiliateData.Clear;
    FWeather.AffiliateData.LastChecked := Now;
  end;

  // Descendants will override this to implement current weather lookup
  Result := False;
end;

procedure TAHMWeatherLookupService.UpdateLocationMeta;
var
  i: Integer;
  sMeta: String;
  Location: TAHMWeatherLocation;
begin
  // Reset location metadata
  FWeather.ClearMetaData(META_WEATHER_SEARCH);

  // Write metadata values for each location match
  for i := 0 to Pred(FWeather.LocationData.Locations.Count) do
  begin
    Location := TAHMWeatherLocation(FWeather.LocationData.Locations.Items[i]);
    sMeta := META_WEATHER_SEARCH_RESULT + IntToStr(i) + META_SEP;
    FWeather.SetMetaData(sMeta + META_WEATHER_SEARCH_ID, Location.LocationID);
    FWeather.SetMetaData(sMeta + META_WEATHER_SEARCH_NAME, Location.LocationName);
  end;

  // Update status metadata
  if FWeather.LocationData.Locations.Count > 0 then
    FWeather.SetMetaData(META_WEATHER_SEARCH_STATUS, META_TRUE)
  else
    FWeather.SetMetaData(META_WEATHER_SEARCH_STATUS, META_FALSE);
end;

procedure TAHMWeatherLookupService.UpdateWeatherMeta;
begin
  // Reset current weather metadata
  FWeather.ClearMetaData(META_WEATHER_CURRENT);
  FWeather.ClearMetaData(META_WEATHER_LOCATION);

  // Update current weather metadata
  with FWeather do
  begin
    // Location data
    SetMetaData(META_WEATHER_LOCATION_ID,       LocationData.CurrentLocation.LocationID);
    SetMetaData(META_WEATHER_LOCATION_NAME,     LocationData.CurrentLocation.LocationName);
    SetMetaData(META_WEATHER_LOCATION_LATT,     LocationData.CurrentLocation.Lattitude);
    SetMetaData(META_WEATHER_LOCATION_LONG,     LocationData.CurrentLocation.Longitude);
    SetMetaData(META_WEATHER_LOCATION_TIMEZONE, LocationData.CurrentLocation.TimeZone);
    SetMetaData(META_WEATHER_LOCATION_CURRTIME, LocationData.CurrentLocation.CurrentTime);
    SetMetaData(META_WEATHER_LOCATION_OBSERV,   LocationData.CurrentLocation.Observatory);
    SetMetaData(META_WEATHER_LOCATION_SUNRISE,   LocationData.CurrentLocation.SunRise);
    SetMetaData(META_WEATHER_LOCATION_SUNSET,    LocationData.CurrentLocation.SunSet);
    // Current data
    SetMetaData(META_WEATHER_CURRENT_LASTUPDATE, CurrentData.LastUpdatedAsString);
    SetMetaData(META_WEATHER_CURRENT_ACT_TEMP,   CurrentData.ActualTemperatureAsString);
    SetMetaData(META_WEATHER_CURRENT_PER_TEMP,   CurrentData.PerceivedTemperatureAsString);
    SetMetaData(META_WEATHER_CURRENT_PRESSURE,   CurrentData.PressureAsString);
    SetMetaData(META_WEATHER_CURRENT_PRESCHNG,   CurrentData.PressureChange);
    SetMetaData(META_WEATHER_CURRENT_VISIBILITY, CurrentData.VisibilityAsString);
    SetMetaData(META_WEATHER_CURRENT_UVINDEX,    CurrentData.UVIndexAsString);
    SetMetaData(META_WEATHER_CURRENT_UVDESCR,    CurrentData.UVDescription);
    SetMetaData(META_WEATHER_CURRENT_DEWPOINT,   CurrentData.DewpointAsString);
    SetMetaData(META_WEATHER_CURRENT_MOONIMG,    CurrentData.MoonImage);
    SetMetaData(META_WEATHER_CURRENT_MOONDESCR,  CurrentData.MoonDescription);
    // Standard data
    SetMetaData(META_WEATHER_CURRENT_IMAGE,  CurrentData.Weather.Image);
    SetMetaData(META_WEATHER_CURRENT_DESCRIPTION,  CurrentData.Weather.Description);
    SetMetaData(META_WEATHER_CURRENT_WINDSPEED,  CurrentData.Weather.WindSpeedAsString);
    SetMetaData(META_WEATHER_CURRENT_WINDGUSTS,  CurrentData.Weather.WindGustsAsString);
    SetMetaData(META_WEATHER_CURRENT_WINDDIRECT, CurrentData.Weather.WindDirection);
    SetMetaData(META_WEATHER_CURRENT_HUMIDITY,  CurrentData.Weather.HumidityAsString);
  end;
end;

procedure TAHMWeatherLookupService.UpdateForecastMeta;
var
  i: Integer;
  sMeta, sMetaPart: String;
  Forecast: TAHMWeatherForecastData;
begin
  with FWeather do
  begin
    // Reset forecast metadata
    ClearMetaData(META_WEATHER_FORECAST);

    // Write common forecast metadata
    SetMetaData(META_WEATHER_FORECAST_LASTUPDATE, ForecastData.LastUpdatedAsString);

    // Write metadata values for each days forecast
    for i := 0 to Pred(ForecastData.Forecasts.Count) do
    begin
      Forecast := TAHMWeatherForecastData(ForecastData.Forecasts.Items[i]);
      sMeta := META_WEATHER_FORECAST_FORECAST + IntToStr(i) + META_SEP;
      SetMetaData(sMeta + META_WEATHER_FORECAST_DAY, Forecast.Day);
      SetMetaData(sMeta + META_WEATHER_FORECAST_DATE, Forecast.Date);
      SetMetaData(sMeta + META_WEATHER_FORECAST_TEMP_LO, Forecast.LowTemperatureAsString);
      SetMetaData(sMeta + META_WEATHER_FORECAST_TEMP_HI, Forecast.HighTemperatureAsString);
      SetMetaData(sMeta + META_WEATHER_FORECAST_SUNRISE, Forecast.SunRise);
      SetMetaData(sMeta + META_WEATHER_FORECAST_SUNSET, Forecast.SunSet);

      // Standard AM data
      sMetaPart := sMeta + META_WEATHER_FORECAST_AM;
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_IMAGE, Forecast.ForecastAM.Image);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_DESCRIPTION, Forecast.ForecastAM.Description);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDSPEED, Forecast.ForecastAM.WindSpeedAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDGUSTS, Forecast.ForecastAM.WindGustsAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDDIRECT,Forecast.ForecastAM.WindDirection);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_HUMIDITY, Forecast.ForecastAM.HumidityAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_RAINLIKELY, Forecast.ForecastAM.RainLikelihoodAsString);

      // Standard PM data
      sMetaPart := sMeta + META_WEATHER_FORECAST_PM;
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_IMAGE, Forecast.ForecastPM.Image);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_DESCRIPTION, Forecast.ForecastPM.Description);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDSPEED, Forecast.ForecastPM.WindSpeedAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDGUSTS, Forecast.ForecastPM.WindGustsAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_WINDDIRECT,Forecast.ForecastPM.WindDirection);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_HUMIDITY, Forecast.ForecastPM.HumidityAsString);
      SetMetaData(sMetaPart + META_WEATHER_FORECAST_RAINLIKELY, Forecast.ForecastPM.RainLikelihoodAsString);
    end;
  end;
end;

procedure TAHMWeatherLookupService.UpdateAffiliateMeta;
var
  i: Integer;
  sMeta: String;
  Link: TAHMWeatherLink;
begin
  // Reset affiliate metadata
  FWeather.ClearMetaData(META_WEATHER_LINKS);

  // Write metadata values for each affiliate link
  for i := 0 to Pred(FWeather.AffiliateData.WeatherLinks.Count) do
  begin
    Link := TAHMWeatherLink(FWeather.AffiliateData.WeatherLinks.Items[i]);
    sMeta := META_WEATHER_LINKS_LINK + IntToStr(i) + META_SEP;
    FWeather.SetMetaData(sMeta + META_WEATHER_LINKS_URL, Link.URL);
    FWeather.SetMetaData(sMeta + META_WEATHER_LINKS_TEXT, Link.Description);
  end;
end;

function TAHMWeatherLookupService.CheckLocation(Location: String): Boolean;
begin
  // Check terms of service then lookup location info
  Result := CheckTermsOfService(wlLocation) and ParseLocation(Location);
  UpdateLocationMeta;
end;

function TAHMWeatherLookupService.CheckWeather: Boolean;
begin
  // Check terms of service for each of our weather data types
  FUpdateWeather := CheckTermsOfService(wlCurrent);
  FUpdateForecast := CheckTermsOfService(wlForecast);
  FUpdateAffiliate := CheckTermsOfService(wlAffiliate);

  // If at least one type needs upating then parse weather
  if FUpdateWeather or FUpdateForecast or FUpdateAffiliate then
    Result := ParseWeather
  else
    Result := True;

  // Update metadata
  UpdateWeatherMeta;
  UpdateForecastMeta;
  UpdateAffiliateMeta;
end;

// TAHMWeatherChannelLookupService

constructor TAHMWeatherChannelLookupService.Create(Owner: TAHMWeatherManager);
begin
  inherited;

  // These are partner Id and license key unique to @hm!
  FPartnerID := '1025562774';
  FLicenseKey := 'ae268cdac51afd0e';
end;

function TAHMWeatherChannelLookupService.GetPreferredUnits: String;
begin
  // Determine appropriate units
  if WeatherUnitsInstance.PreferredUnits = wupMetric then
    Result := URL_PARAM_WEATHERDOTCOM_METRIC
  else
    Result := URL_PARAM_WEATHERDOTCOM_STANDARD;
end;

function TAHMWeatherChannelLookupService.ParseLastUpdated(LastUpdated: String): TDateTime;
var
  sDate: String;
  i: Integer;
  DTSettings: TFormatSettings;
begin
  // Remove "Local time" suffix from last update
  sDate := UpperCase(LastUpdated);
  i := Pos(' LOCAL TIME', sDate);
  if i > 0 then sDate := Copy(sDate, 1, Pred(i));

  // Convert date using US English locale (MM/DD/YY format)
  GetLocaleFormatSettings(1033, DTSettings);
  DTSettings.TwoDigitYearCenturyWindow := 50;
  if not TryStrToDateTime(sDate, Result, DTSettings) then
    Result := Now; // default to current date/time if conversion fails
end;

function TAHMWeatherChannelLookupService.ParseImage(WeatherIndex: String): String;
const
  IMAGE_NAMES: array[0..47] of String = (
    IMG_STORM, IMG_STORM, IMG_STORM, IMG_STORM, IMG_STORM, IMG_SNOW_SHOWERS,
    IMG_HAIL_SHOWERS, IMG_SHOWERS, IMG_SHOWERS, IMG_SHOWERS, IMG_RAIN, IMG_RAIN,
    IMG_RAIN, IMG_SNOW, IMG_SNOW, IMG_SNOW, IMG_SNOW, IMG_STORM, IMG_HAIL,
    IMG_DUST, IMG_FOG, IMG_HAZE, IMG_SMOKE, IMG_WINDY, IMG_WINDY, IMG_FRIGID,
    IMG_CLOUDY, IMG_CLOUDY, IMG_CLOUDY, IMG_CLOUDY, IMG_PARTLY_CLOUDY,
    IMG_CLEAR, IMG_SUNNY, IMG_CLEAR, IMG_SUNNY, IMG_STORM, IMG_SUNNY, IMG_STORM,
    IMG_STORM, IMG_SHOWERS, IMG_RAIN, IMG_SNOW, IMG_SNOW, IMG_SNOW, IMG_NA,
    IMG_RAIN, IMG_SNOW, IMG_STORM);
var
  i: Integer;
begin
  // Convert image index to a standard weather image name
  i := StrToIntDef(WeatherIndex, -1);
  if (i < Low(IMAGE_NAMES)) or (i > High(IMAGE_NAMES)) then
    Result := IMG_NA
  else
    Result := IMAGE_NAMES[i];
end;

function TAHMWeatherChannelLookupService.CheckTermsOfService(Request: TAHMWeatherDataType): Boolean;
const
  REFRESH_CURRENT = 30;    // 30 minutes for current conditions
  REFRESH_FORECAST = 120;  // 2hrs for forecast
  REFRESH_AFFILIATE = 720; // 12hrs for affiliate link lookup

  function UpdateNow(Checked: TDateTime; Minutes: Integer): Boolean;
  begin
    Result := (Checked = 0) or not WithinPastMinutes(Now, Checked, Minutes);
  end;
begin
  // First check we have a current internet connection
  Result := False;
  if not FWeather.IsConnected then Exit;

  // Check whether another update is permitted according to weather.com terms
  // of service. For info on this see guide.pdf included in SDK
  case Request of
    wlLocation:  Result := True; // call this on demand
    wlCurrent:   Result := UpdateNow(FWeather.CurrentData.LastChecked, REFRESH_CURRENT);
    wlForecast:  Result := UpdateNow(FWeather.ForecastData.LastChecked, REFRESH_FORECAST);
    wlAffiliate: Result := UpdateNow(FWeather.AffiliateData.LastChecked, REFRESH_AFFILIATE);
  end;
end;

function TAHMWeatherChannelLookupService.ParseLocation(Location: String): Boolean;
var
  sURL: String;
  RootNode, LocnNode: TAHMXMLNode;
begin
  // Call inherited method to clear existing data
  Result := inherited ParseLocation(Location);

  Location := FWeather.EncodeParams(Location);
  sURL := Format(URL_WEATHERDOTCOM_LOCATION, [Location]);
  LocnNode := nil;

  // Lookup location info & parse XML
  if RetrieveXML(sURL) then
    if FXML.FindRootElement(RootNode, TAG_WEATHERDOTCOM_SEARCH) then
    begin
      while FXML.FindElement(RootNode, LocnNode, TAG_WEATHERDOTCOM_LOCATION) do
        with TAHMWeatherLocation(FWeather.LocationData.Locations.Add) do
        begin
          LocationId := FXML.GetAttribute(TAG_WEATHERDOTCOM_ID_ATTR);
          LocationName := FXML.GetValue;
        end;

      // Okay, parsed successfully
      Result := True;
    end
    else
      FWeather.LogMessage(llError, 'Failed to parse Weather location search XML response');
end;

function TAHMWeatherChannelLookupService.ParseWeather: Boolean;
var
  sURL: String;
  RootNode, CurrentNode: TAHMXMLNode;
begin
  // Call inherited method to clear existing data
  Result := inherited ParseWeather;

  with FWeather do
  begin
    // Build our URL to retrieve required weather components
    sURL := Format(URL_WEATHERDOTCOM_BASE, [LocationData.CurrentLocation.LocationID,
                                            FPartnerID, FLicenseKey, GetPreferredUnits]);
    if ShouldUpdateWeather then sURL := sURL + URL_WEATHERDOTCOM_CURRENT;
    if ShouldUpdateForecast then sURL := sURL + Format(URL_WEATHERDOTCOM_FORECAST, [FForecastDays]);
    if ShouldUpdateAffiliate then sURL := sURL + URL_WEATHERDOTCOM_LINKS;
  end;

  // Lookup current weather data & parse XML
  if RetrieveXML(sURL) then
    if FXML.FindRootElement(RootNode, TAG_WEATHERDOTCOM_WEATHER) then
    begin
      CurrentNode := nil;
      while FXML.NextElement(RootNode, CurrentNode) do
      begin
        // What do we have here?
        if FXML.CurrentElement = TAG_WEATHERDOTCOM_HEADER then ParseDetailHeader(CurrentNode)
        else if FXML.CurrentElement = TAG_WEATHERDOTCOM_LOCATION then ParseDetailLocation(CurrentNode)
        else if FXML.CurrentElement = TAG_WEATHERDOTCOM_CURRENT then ParseDetailConditions(CurrentNode)
        else if FXML.CurrentElement = TAG_WEATHERDOTCOM_FORECASTS then ParseDetailForecasts(CurrentNode)
        else if FXML.CurrentElement = TAG_WEATHERDOTCOM_LINKS then ParseDetailAffiliates(CurrentNode);

        // Okay, parsed successfully
        Result := True;
      end
    end
    else
      FWeather.LogMessage(llError, 'Failed to parse Weather current conditions XML response');
end;

procedure TAHMWeatherChannelLookupService.ParseDetailHeader(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within header node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_UNITOFTEMP then
      if FXML.GetValue = 'F' then WeatherUnitsInstance.TempUnit := wtDegreesF
                             else WeatherUnitsInstance.TempUnit := wtDegreesC
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UNITOFDIST then
      if FXML.GetValue = 'mi' then WeatherUnitsInstance.DistanceUnit := wdMiles
                              else WeatherUnitsInstance.DistanceUnit := wdKilometres
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UNITOFSPEED then
      if FXML.GetValue = 'mph' then WeatherUnitsInstance.SpeedUnit := wsMPH
                               else WeatherUnitsInstance.SpeedUnit := wsKMH
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UNITOFRAIN then
      if FXML.GetValue = 'in' then WeatherUnitsInstance.RainfallUnit := wrInch
                              else WeatherUnitsInstance.RainfallUnit := wrMM
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UNITOFPRES then
      if FXML.GetValue = 'in' then WeatherUnitsInstance.PressureUnit := wpInches
                              else WeatherUnitsInstance.PressureUnit := wpMillibars;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailLocation(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within location node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_NAME then
      FWeather.LocationData.CurrentLocation.LocationName := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_CURRENTTIME then
      FWeather.LocationData.CurrentLocation.CurrentTime := LowerCase(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_LATTITUDE then
      FWeather.LocationData.CurrentLocation.Lattitude := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_LONGITUDE then
      FWeather.LocationData.CurrentLocation.Longitude := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_SUNRISE then
      FWeather.LocationData.CurrentLocation.Sunrise := LowerCase(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_SUNSET then
      FWeather.LocationData.CurrentLocation.Sunset := LowerCase(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_TIMEZONE then
      FWeather.LocationData.CurrentLocation.Timezone := FXML.GetValue;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailConditions(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within current conditions node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_LASTUPDATE then
      FWeather.CurrentData.LastUpdated := ParseLastUpdated(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_OBSERVATORY then
      FWeather.LocationData.CurrentLocation.Observatory := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_TEMPERATURE then
      FWeather.CurrentData.ActualTemperature := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_FEELSLIKE then
      FWeather.CurrentData.PerceivedTemperature := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_DESCRIPTION then
      FWeather.CurrentData.Weather.Description := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_IMAGE then
      FWeather.CurrentData.Weather.Image := ParseImage(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_BAROMETER then
      ParseDetailBarometer(CurrentNode)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_WIND then
      ParseDetailWind(CurrentNode, FWeather.CurrentData.Weather)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_HUMIDITY then
      FWeather.CurrentData.Weather.Humidity := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_VISIBILITY then
      FWeather.CurrentData.Visibility := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UV then
      ParseDetailUV(CurrentNode)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_DEWPOINT then
      FWeather.CurrentData.Dewpoint := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_MOON then
      ParseDetailMoon(CurrentNode)
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailBarometer(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within barometer node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_PRESSURE then
      FWeather.CurrentData.Pressure := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_PRESSURECHG then
      FWeather.CurrentData.PressureChange := FXML.GetValue;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailWind(RootNode: TAHMXMLNode; WeatherData: TAHMWeatherStandardData);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within wind node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_WINDSPEED then
      WeatherData.WindSpeed := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_WINDGUST then
      WeatherData.WindGusts := StrToFloatDef(FXML.GetValue, 0.0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_WINDDIRECTN then
      WeatherData.WindDirection := FXML.GetValue;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailUV(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within UV node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_UVINDEX then
      FWeather.CurrentData.UVIndex := StrToIntDef(FXML.GetValue, 0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_UVDESCR then
      FWeather.CurrentData.UVDescription := FXML.GetValue;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailMoon(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within Moon node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_MOONIMAGE then
      FWeather.CurrentData.MoonImage := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_MOONDESCR then
      FWeather.CurrentData.MoonDescription := FXML.GetValue;
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailForecasts(RootNode: TAHMXMLNode);
var
  CurrentNode: TAHMXMLNode;
begin
  // Parse nodes within forecasts node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_LASTUPDATE then
      FWeather.ForecastData.LastUpdated := ParseLastUpdated(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_FORECAST then
      ParseDetailForecast(CurrentNode);
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailForecast(RootNode: TAHMXMLNode);
var
  Forecast: TAHMWeatherForecastData;
  CurrentNode: TAHMXMLNode;
begin
  // Add new weather forecast item
  Forecast := TAHMWeatherForecastData(FWeather.ForecastData.Forecasts.Add);

  // Parse root node attributes first
  Forecast.Day := FXML.GetAttribute(TAG_WEATHERDOTCOM_DAY_ATTR);
  Forecast.Date := FXML.GetAttribute(TAG_WEATHERDOTCOM_DATE_ATTR);

  // Parse nodes within forecast node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_TEMP_HI then
      Forecast.HighTemperature := StrToIntDef(FXML.GetValue, 0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_TEMP_LOW then
      Forecast.LowTemperature := StrToIntDef(FXML.GetValue, 0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_SUNRISE then
      Forecast.SunRise := LowerCase(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_SUNSET then
      Forecast.SunSet := LowerCase(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_DAY_PART then
      ParseDetailForecastPart(CurrentNode)
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailForecastPart(RootNode: TAHMXMLNode);
var
  Forecast: TAHMWeatherForecastData;
  WeatherData: TAHMWeatherStandardData;
  CurrentNode: TAHMXMLNode;
begin
  // Get previously added weather forecast item
  with FWeather.ForecastData.Forecasts do
    Forecast := TAHMWeatherForecastData(Items[Pred(Count)]);

  // Parse root node attributes to determine AM/PM forecast
  if FXML.GetAttribute(TAG_WEATHERDOTCOM_AMPM_ATTR) = 'd' then
    WeatherData := Forecast.ForecastAM else WeatherData := Forecast.ForecastPM;

  // Parse nodes within forecast part node
  CurrentNode := nil;
  while FXML.NextElement(RootNode, CurrentNode) do
  begin
    // What do we have here?
    if FXML.CurrentElement = TAG_WEATHERDOTCOM_IMAGE then
      WeatherData.Image := ParseImage(FXML.GetValue)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_DESCRIPTION then
      WeatherData.Description := FXML.GetValue
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_WIND then
      ParseDetailWind(CurrentNode, WeatherData)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_CHANCERAIN then
      WeatherData.RainLikelihood := StrToIntDef(FXML.GetValue, 0)
    else if FXML.CurrentElement = TAG_WEATHERDOTCOM_HUMIDITY then
      WeatherData.Humidity := StrToFloatDef(FXML.GetValue, 0.0);
  end;
end;

procedure TAHMWeatherChannelLookupService.ParseDetailAffiliates(RootNode: TAHMXMLNode);
var
  Link: TAHMWeatherLink;
  LinkNode, DetailNode: TAHMXMLNode;
begin
  // Iterate through found links
  LinkNode := nil;
  while FXML.FindElement(RootNode, LinkNode, TAG_WEATHERDOTCOM_LINK) do
  begin
    // Create new link instance
    Link := TAHMWeatherLink(FWeather.AffiliateData.WeatherLinks.Add);
    DetailNode := nil;

    // Parse link detail (URL and text parts)
    while FXML.NextElement(LinkNode, DetailNode) do
    begin
      if FXML.CurrentElement = TAG_WEATHERDOTCOM_LINKURL then
        Link.URL := FXML.GetValue
      else if FXML.CurrentElement = TAG_WEATHERDOTCOM_LINKTEXT then
        Link.Description := FXML.GetValue
    end;
  end;  
end;

// TAHMWeatherManager

constructor TAHMWeatherManager.Create(Server: IMediateServer);
var
  ConfigDir: WideString;
begin
  inherited Create;

  // Store server instance & build persistence filename
  FMediate := Server;
  Server.GetConfigDirectory(ConfigDir);
  FFilename := ConfigDir + FILE_WEATHER;

  // Create global weather units instance
  WeatherUnitsInstance := TAHMWeatherUnits.Create;
  FWeatherUnits := WeatherUnitsInstance;

  FWeatherLookup:= TAHMWeatherChannelLookupService.Create(Self);
  FWeatherLocation := TAHMWeatherLocationData.Create;
  FWeatherAffiliate := TAHMWeatherAffiliateData.Create;
  FWeatherCurrent := TAHMWeatherCurrentData.Create;
  FWeatherForecasts := TAHMWeatherForecasts.Create;

  // Load cache so we can populate our configuration settings
  TAHMPersistenceHelper.LoadObject(Self, FFilename);
  ValidateConfiguration;

  LogMessage(llDebug, 'loaded plugin successfully');
end;

destructor TAHMWeatherManager.Destroy;
begin
  FWeatherLookup.Free;
  FWeatherLocation.Free;
  FWeatherAffiliate.Free;
  FWeatherCurrent.Free;
  FWeatherForecasts.Free;
  FreeAndNil(WeatherUnitsInstance);
  LogMessage(llDebug, 'unloaded plugin successfully');

  inherited;
end;

procedure TAHMWeatherManager.LogMessage(Level: LoggingLevel; LogMessage: String);
begin
  FMediate.LogMessage(Level, LogMessage);
end;

function TAHMWeatherManager.GetHTTPString(const URL: String): String;
var
  Response: WideString;
begin
  Result := '';
  
  with FMediate as IMediateInternet do
  begin
    if GetHTTPString(URL, Response) = S_OK then
      Result := Response;
  end;
end;

function TAHMWeatherManager.IsConnected: Boolean;
var
  Connected: WordBool;
begin
  Result := False;

  with FMediate as IMediateInternet do
  begin
    if IsConnected(Connected) = S_OK then
      Result := Connected;
  end;
end;

function TAHMWeatherManager.EncodeParams(const Params: String): String;
var
  Encoded: WideString;
begin
  Result := '';

  with FMediate as IMediateInternet do
  begin
    if EncodeParams(Params, Encoded) = S_OK then
      Result := Encoded;
  end;
end;

procedure TAHMWeatherManager.ClearMetaData(Key: String);
begin
  FMediate.ClearMetaData(Key);
end;

procedure TAHMWeatherManager.SetMetaData(Key, Value: String);
begin
  FMediate.SetMetaData(Key, Value);
end;

function TAHMWeatherManager.SearchLocation(Location: String): Boolean;
begin
  Result := FWeatherLookup.CheckLocation(Location);
end;

function TAHMWeatherManager.ValidateConfiguration: Boolean;
var
  sLocn: String;
  sMetric, sImperial: WideString;
begin
  // Populate our weather configuration settings metadata
  sLocn := FWeatherLocation.CurrentLocation.LocationName;
  FMediate.SetMetaData(META_WEATHER_CONFIG_LOCATION_NAME, sLocn);
  sLocn := FWeatherLocation.CurrentLocation.LocationID;
  FMediate.SetMetaData(META_WEATHER_CONFIG_LOCATION_ID, sLocn);
  if FWeatherUnits.PreferredUnits = wupMetric then
    FMediate.SetMetaData(META_WEATHER_CONFIG_UNIT, META_WEATHER_CONFIG_METRIC)
  else
    FMediate.SetMetaData(META_WEATHER_CONFIG_UNIT, META_WEATHER_CONFIG_IMPERIAL);

  // Allow mediate to customise our default metric & imperial strings
  FMediate.GetLanguageText(TEXT_WEATHER_CONFIG_METRIC, sMetric);
  FMediate.GetLanguageText(TEXT_WEATHER_CONFIG_IMPERIAL, sImperial);
  if sMetric = '' then sMetric := META_WEATHER_CONFIG_METRIC;
  if sImperial = '' then sImperial := META_WEATHER_CONFIG_IMPERIAL;

  // Populate available units configuration values
  FMediate.SetMetaData(META_WEATHER_CONFIG_UNITS_METRIC, sMetric);
  FMediate.SetMetaData(META_WEATHER_CONFIG_UNITS_IMPERIAL, sImperial);

  // Do we have a location configured?
  Result := (sLocn <> '');
end;

procedure TAHMWeatherManager.UpdateWeatherData;
begin
  LogMessage(llDebug, 'updating weather details...');

  // Clear any current weather data then attempt to load from cache
  FWeatherAffiliate.Clear;
  FWeatherCurrent.Clear;
  FWeatherForecasts.Clear;
  TAHMPersistenceHelper.LoadObject(Self, FFilename);

  // Check configuration first
  if not ValidateConfiguration then
  begin
    LogMessage(llWarning, 'weather location has not yet been configured');
    FMediate.TriggerAction(EVENT_WEATHER_NOLOCN);
    Exit;
  end;

  // Update our weather data within feed terms of service
  with FWeatherLookup do
    if CheckWeather then
    begin
      LogMessage(llDebug, 'weather details updated');
      FMediate.TriggerAction(EVENT_WEATHER_UPDATED);
    end
    else
    begin
      LogMessage(llWarning, 'weather update failed');
      FMediate.TriggerAction(EVENT_WEATHER_FAILED);
    end;

  // Write current weather data to cache
  TAHMPersistenceHelper.SaveObject(Self, FFilename);
end;

procedure TAHMWeatherManager.SearchWeatherLocations;
var
  sLocation: WideString;
begin
  LogMessage(llDebug, 'searching weather locations...');

  // Get search request parameter from metadata & perform search
  FMediate.GetMetaData(META_WEATHER_SEARCH_LOCATION, sLocation);
  SearchLocation(sLocation);

  LogMessage(llDebug, 'weather location search complete');
  FMediate.TriggerAction(EVENT_SEARCH_COMPLETE);
end;

procedure TAHMWeatherManager.UpdateConfiguration;
var
  sLocation, sUnits: WideString;
begin
  LogMessage(llDebug, 'updating weather configuration...');

  // Get configuration parameters from metadata
  FMediate.GetMetaData(META_WEATHER_CONFIG_LOCATION_ID, sLocation);
  FMediate.GetMetaData(META_WEATHER_CONFIG_UNIT, sUnits);

  // Update current configuration with new values
  if sLocation <> '' then
    FWeatherLocation.CurrentLocation.LocationID := sLocation;

  // Update preferred units with new values
  if sUnits = META_WEATHER_CONFIG_METRIC then
     FWeatherUnits.PreferredUnits := wupMetric
  else
     FWeatherUnits.PreferredUnits := wupImperial;

  // Invalidate current weather and overwrite cached values
  FWeatherCurrent.Clear;
  FWeatherForecasts.Clear;
  TAHMPersistenceHelper.SaveObject(Self, FFilename);

  LogMessage(llDebug, 'weather configuration settings updated');
end;


end.
